Skip to main content

Tests

Backend tests are written with Jest and executed against a set of emulators for core services

Prerequisites

You will need to support the running of Firebase Emulators

Running Locally

yarn workspace functions test:watch

This will startup the firebase emulator suite, compile functions, and watch for any changes to function or spec test files

info

The emulator console can be viewed on localhost:4001

To run just a single test the interactive prompts can be used to provide a matching filename path, e.g.

$ yarn workspace functions test:watch

$ Watch Usage
> Press p to filter by a filename regex pattern

$ pattern › mySpec

This will automatically watch for changes to any files that match the regular expression /mySpec/

Writing Tests

Tests are written in Jest, with filenames ending .spec.ts

Utilities and Examples

Mocking Methods

Mocks are a useful tool to reduce the number of additional services a test interacts with, making them more resilient to changes in the codebase.

There are various ways to achieve this, many of which are outlines in the Jest Mock Functions documentation

A few methods commonly used in the codebase include:

Imports Return a set of mock methods or utilities in place of an import. An example from the frontend code is when we want to avoid importing all stores, but instead just mock a single method

jest.mock('src/common/hooks/useCommonStores', () => {
return {
useCommonStores() {
return {
stores: {
userStore: {
fetchAllVerifiedUsers: jest.fn(),
},
},
}
},
}
})

Class Methods Replace specific class method with an alternative mock method. E.g. if the code relates to the active user, a mock stub could be used instead. An artificial example:

import UserMethods from './userMethods'

UserMethods.activeUser = jest.fn().mockReturnValue({ id: 'fake_user' })

Replace or extend multiple class methods with mocks Similarly, entire mock classes can be used where appropriate

import UserMethods from './userMethods'

class MockUserMethods implements Partial<UserMethods>{
activeUser: () => jest.fn().mockReturnValue({ id: 'fake_user' })
setUser: () => jest.fn()
}

Execute Functions Directly

It is possible to directly execute any function within the test environment without its required trigger. A utility FirebaseEmulatedTest class is used to wrap the function invocation so that it can be used async, e.g.

myFunction.ts

import functions from 'firebase-functions'

exports.default = functions.firestore
.document(`mockEndpoint/{id}`)
.onUpdate((change) => {
return doSomething(change)
})

myFunction.spec.ts

import { FirebaseEmulatedTest } from '../test/Firebase/emulator'
import myFunction from './myFunction.ts'

const beforeData = { field: 'initialValue' }
const afterData = { field: 'changedValue' }

const change = FirebaseEmulatedTest.mockFirestoreChangeObject(
beforeData,
afterData,
'mockEndpoint',
'mockDocId',
)

await FirebaseEmulatedTest.run(myFunction, change)

Additional utilities can also be used to provide a mock of the triggering context. In the example above a mock before and after data snapshot is created to mimic the firestore document change trigger used by the function

Seed and Teardown Data

The test utilities also have methods for seeding and clearing the Firestore DB if required for tests.

myFunction.spec.ts

beforeEach(async () => {
await FirebaseEmulatedTest.seedFirestoreDB('users', [{ _id: 'user1 ' }])
})
afterEach(async () => {
await FirebaseEmulatedTest.clearFirestoreDB()
})

Additionally all emulator data will be cleared when scripts are terminated

caution

If your test function interacts directly with an empty database endpoint it may throw an error
(various issues on github, should review in the future)

The easiest workaround is to seed the endpoint without any docs, which will allow the emulators to return an empty array from queries.

await FirebaseEmulatedTest.seedFirestoreDB('empty_endpoint',[])

Production Data

There may also be some cases where methods want to be tested against production data to check for any additional edge-cases or give a preview new feature development.

There currently isn't a single automated way to do this, however you can see an example of the manual steps involved in the test_functions step of the CircleCI pipeline

Alternatively developers can follow the steps in Firebase Emulators Docker to run the docker emulators locally and manually invoke functions